import * as util from "util";
import "colors";
import * as fs from "fs";
import * as path from "path";

const walk = require("walk");
const readFile = util.promisify<string, string, string>(fs.readFile);

export interface IRef {
  ref: string;
  file: string;
  loc: number;
}

// import v from "case1"
// import { v } from "case2"
// import "case3"
// export v from "case4"
// export { v } from "case5"
// require("case6")
// const a = require("case7")
// import "style-loader!css-loader!less-loader!../b/some";

const reg1 = /^\s*((?:import|export)\s+(?:[a-zA-Z0-9_${}*\s,]+\s+from\s+)?(?:'|"))(.+)(?:'|");?$/gm;
const reg2 = /^((?!\/\/)(?:.(?!\/\/))*?require\((?:'|"))(.+)(?:'|")\)/gm;

/**
 * `a` and `b` must be directory and existing
 * ensure all files inside `a` do not keep any cross-references inside `b` and vice	versa.
 */
export class Validator {
  private a: string;
  private b: string;
  private an: string;
  private bn: string;

  constructor(a: string, b: string) {
    this.a = a;
    this.b = b;
    this.an = path.parse(this.a).name;
    this.bn = path.parse(this.b).name;
  }

  async extractReferences(file: string) {
    const src = await readFile(file, "utf8");
    const refs: IRef[] = [];

    const exec = (str: string, reg: RegExp) => {
      while (true) {
        const matches = reg.exec(str);
        if (matches === null) break;
        let ref = matches[2];
        let loc = matches.index + matches[1].length;
        // if webpack loader syntax
        if (ref.indexOf("!") !== -1) {
          const i = ref.lastIndexOf("!");
          ref = i === -1 ? ref : ref.slice(i + 1);
          loc = i === -1 ? loc : loc + i + 1;
        }
        ref = path.resolve(path.parse(file).dir, ref);
        refs.push({
          ref,
          file,
          loc
        });
      }
    };

    exec(src, reg1);
    exec(src, reg2);
    return refs;
  }

  async listFiles(dir: string) {
    const walker = walk.walk(dir);
    return new Promise<string[]>((resolve, reject) => {
      const files: string[] = [];
      walker.on("file", async (root: string, stats: any, next: any) => {
        if (stats.isFile() && /\.(json|tsx?|jsx?)$/.test(stats.name)) {
          files.push(path.join(root, stats.name));
        }
        next();
      });

      walker.on("directory", (root: string, stats: any, next: any) => {
        next();
      });

      walker.on("error", (root: string, stats: any) => {
        reject(stats.error);
      });

      walker.on("end", () => {
        resolve(files);
      });
    });
  }

  async listNotSatisfied(a: string, b: string) {
    const files = await this.listFiles(a);
    let bads: IRef[] = [];
    for (let i = 0, len = files.length; i < len; i++) {
      const refs = await this.extractReferences(files[i]);
      bads = bads.concat(refs.filter(r => r.ref.startsWith(b)));
    }
    return bads;
  }

  lineNumAtIdx(src: string, idx: number) {
    let num = 1;
    for (let i = 0, len = src.length; i <= idx; i++) {
      const ch = src[i];
      if (ch === "\r") {
        if (src[i + 1] === "\n") {
          i++;
        }
        num++;
      } else if (ch === "\n") {
        num++;
      }
    }
    return num;
  }

  async visualizeRef(ref: IRef) {
    const src = await readFile(ref.file, "utf8");
    let begin = ref.loc;
    let end = ref.loc;
    for (; begin > 0; begin--) {
      if (src[begin] === "\r" || src[begin] === "\n") {
        begin += 1;
        break;
      }
    }
    for (const len = src.length; end < len; end++) {
      if (src[end] === "\r" || src[end] === "\n") {
        break;
      }
    }
    const line = src.slice(begin, end);
    const lineNum = this.lineNumAtIdx(src, ref.loc);
    const offset = ref.loc - begin;
    const pad = offset ? " ".repeat(offset + 2) : "  ";
    let msg = `${ref.file}:${lineNum}:${offset}`.bold + "";
    msg += "\n  " + line + "\n";
    msg += pad + "^".red;
    return msg;
  }

  async run() {
    const a = await this.listNotSatisfied(this.a, this.b);
    const b = await this.listNotSatisfied(this.b, this.a);
    const tasks: Array<Promise<any>> = [];
    a.forEach(r => tasks.push(this.visualizeRef(r)));
    b.forEach(r => tasks.push(this.visualizeRef(r)));
    return Promise.all(tasks);
  }
}
